/*
 * MIT License
 *
 * Copyright (c) 2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <cstdint>
#include <fstream>
#include <string>
#include <string_view>

#include <ark_native_engine.h>
#include <native_engine/native_utils.h>
#include <utils/log.h>

#include "ecmascript/napi/include/jsnapi_expo.h"
#include "js_utils/js_childprocess.h"
#include "js_utils/js_console.h"
#include "js_utils/js_fetch.h"
#include "js_utils/js_file.h"
#include "js_utils/js_process.h"
#include "js_utils/js_textdecoder.h"
#include "js_utils/js_textencoder.h"
#include "js_utils/js_timer.h"
#include "js_utils/js_uri.h"
#include "js_utils/js_utils.h"
#include "runner_utils/log.h"
#include "runner_utils/utils.h"

#include "napi_runner/napi_runner.h"

// mock method: use to set napi log level
extern "C" void SetLogLevel(LogLevel level);

#define EXTERNAL_TS_MODULE(XX) \
    /* basic utils */          \
    XX(napi)                   \
    XX(inner_errors)           \
    XX(uni_kv_store)           \
    /* exported modules */     \
    XX(base64)                 \
    XX(console)                \
    XX(event_target)           \
    /* dep event_target */     \
    XX(readable_stream)        \
    /* dep UniversalKVStroe */ \
    XX(formdata)               \
    /* dep UniversalKVStroe */ \
    XX(header)                 \
    XX(process)                \
    /* dep process */          \
    XX(child_process)          \
    XX(require_napi)           \
    XX(text_decoder)           \
    XX(text_encoder)           \
    /* dep UniversalKVStroe */ \
    XX(url_search_params)      \
    /* dep URLSearchParams */  \
    XX(url)

#ifndef DEBUG_JS_MODULE
#define BIND_MODULE_BINARY(name)                      \
    extern const char _binary_##name##_abc_content[]; \
    extern const int32_t _binary_##name##_abc_length;
EXTERNAL_TS_MODULE(BIND_MODULE_BINARY)
#undef BIND_MODULE_BINARY
#endif

#define CREATE_ERROR(vm, msg, type) panda::Exception::type((vm), panda::StringRef::NewFromUtf8((vm), (msg)))
#define N_RUNNER_THROW_ERROR(vm, msg) panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, Error))
#define N_RUNNER_THROW_RANGE_ERROR(vm, msg) panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, RangeError))
#define N_RUNNER_THROW_TYPE_ERROR(vm, msg) panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, TypeError))
#define N_RUNNER_THROW_REFERENCE_ERROR(vm, msg) \
    panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, ReferenceError));
#define N_RUNNER_THROW_SYNTAX_ERROR(vm, msg) panda::JSNApi::ThrowException((vm), CREATE_ERROR(vm, msg, SyntaxError));

std::vector<char> LoadABCFile(std::string name)
{
#ifndef TS_MODULE_OUT_DIR
#define TS_MODULE_OUT_DIR "."
#endif

    std::ifstream file(std::string(TS_MODULE_OUT_DIR) + "/" + name, std::ios::binary);
    if (!file)
    {
        DEFAULT_LOGGER(FATAL) << "Cannot load module: " << name;
    }
    file.seekg(0, std::ios::end);
    std::streamsize size = file.tellg();
    file.seekg(0, std::ios::beg);

    std::vector<char> buffer(size);

    if (!file.read(buffer.data(), size))
    {
        DEFAULT_LOGGER(FATAL) << "Failed to load module: " << name;
    }

    file.close();
    return buffer;
}

int NapiRunner::processArgc_{0};
int NapiRunner::scriptArgIndex_{0};
const char **NapiRunner::processArgs_{nullptr};

NapiRunner::NapiRunner(panda::ecmascript::JSRuntimeOptions *runtimeOptions)
{
    vm_ = JSNApi::CreateEcmaVM(*runtimeOptions);
    if (vm_ == nullptr)
    {
        DEFAULT_LOGGER(ERROR) << "Cannot Create EcmaVM";
        return;
    }

    env_ = new ArkNativeEngine(vm_, nullptr, false);
    if (!env_)
    {
        DEFAULT_LOGGER(ERROR) << "Failed to init ArkNativeEngine";
        return;
    }
    env_->SetApiVersion(20);
}

NapiRunner::~NapiRunner()
{
    if (timer_)
        timer_->Deinit();
    uv_check_stop(&checkHandle_);
    JSNApi::TriggerGC(vm_, JSNApi::TRIGGER_GC_TYPE::FULL_GC);
    JSNApi::TriggerGC(vm_, JSNApi::TRIGGER_GC_TYPE::SHARED_FULL_GC);
    delete env_;
    env_ = nullptr;
    if (timer_)
    {
        delete timer_;
        timer_ = nullptr;
    }
    for (auto it : loadedModules_)
        it.second.FreeGlobalHandleAddr();
    JSNApi::DestroyJSVM(vm_);
    vm_ = nullptr;
}

int NapiRunner::InitLoop()
{
    if (!vm_)
        return -1;
    if (!env_)
        return -2;
    uv_loop_s *loop = env_->GetUVLoop();
    if (!loop)
        return -3;
    uv_walk(
        loop, [](uv_handle_t *handle, void *) { uv_unref(handle); }, nullptr);
    uv_check_init(loop, &checkHandle_);
    uv_unref(reinterpret_cast<uv_handle_t *>(&checkHandle_));
    checkHandle_.data = this;
    uv_check_start(&checkHandle_, [](uv_check_t *handle) {
        auto that = reinterpret_cast<NapiRunner *>(handle->data);
        if (!JSNApi::HasPendingException(that->GetVM()))
        {
            JSNApi::ExecutePendingJob(that->GetVM());
        }
    });
    return 0;
}

void NapiRunner::InitNativeModule()
{
    napi_env env = GetEnv();
    napi_value global = nullptr;
    napi_get_global(env, &global);

    {
        Local<panda::ObjectRef> globalRef = panda::JSNApi::GetGlobalObject(vm_);
#ifdef BUILD_OUTPUT_DIR
        globalRef->Set(vm_, panda::StringRef::NewFromUtf8(vm_, "_buildDir"),
                       panda::StringRef::NewFromUtf8(vm_, BUILD_OUTPUT_DIR));
#endif
        globalRef->Set(vm_, panda::StringRef::NewFromUtf8(vm_, "requireNapi"),
                       panda::FunctionRef::New(vm_, RequireNapi, nullptr, static_cast<void *>(this)));
        globalRef->Set(vm_, panda::StringRef::NewFromUtf8(vm_, "preLoad"),
                       panda::FunctionRef::New(vm_, PreLoad, nullptr, static_cast<void *>(this)));
        globalRef->Set(vm_, panda::StringRef::NewFromUtf8(vm_, "_innerExecute"),
                       panda::FunctionRef::New(vm_, InnerExecute, nullptr, static_cast<void *>(this)));
    }

    JsConsole::BindToGlobal(env, global);
    JsUtils::Init(env, global);
    JsFile::Init(env, global);
    JsTextDecoder::Init(env, global);
    JsTextEncoder::Init(env, global);
    JsUri::Init(env, global);

    timer_ = JsTimer::BindToGlobal(env, global);
    timer_->Init();
    JsProcess::Init(env, global, processArgc_, scriptArgIndex_, processArgs_, getpid() == gettid());
    JsChildProcess::Init(env, global);
}

void NapiRunner::LoadTSModule()
{
#ifndef DEBUG_JS_MODULE
#define LOAD_MODULE_BUFFER(name) std::string_view buffer(_binary_##name##_abc_content, _binary_##name##_abc_length)
#else
#define LOAD_MODULE_BUFFER(name)                \
    auto rawBuffer = LoadABCFile(#name ".abc"); \
    std::string_view buffer(rawBuffer.data(), rawBuffer.size())
#endif
    std::string entryPointName(PANDA_MAIN_FUNCTION);
#define LOAD_TS_MODULE(name)                                                                                           \
    do                                                                                                                 \
    {                                                                                                                  \
        DEFAULT_LOGGER(DEBUG) << "start to load module: " #name;                                                       \
        LOAD_MODULE_BUFFER(name);                                                                                      \
        panda::LocalScope scope(GetVM());                                                                              \
        if (!JSNApi::Execute(GetVM(), reinterpret_cast<const uint8_t *>(buffer.data()), buffer.size(), entryPointName, \
                             #name, false))                                                                            \
        {                                                                                                              \
            DEFAULT_LOGGER(FATAL) << "Failed to load module: " #name;                                                  \
        }                                                                                                              \
    } while (0);

    EXTERNAL_TS_MODULE(LOAD_TS_MODULE)
#undef LOAD_MODULE_BUFFER
#undef LOAD_TS_MODULE
}

int NapiRunner::RunScript(const std::string &path)
{
    if (!JSNApi::Execute(vm_, path, "_GLOBAL::func_main_0"))
    {
        return -1;
    }
    if (JSNApi::HasPendingException(vm_))
        return -2;
    return 0;
}

int NapiRunner::RunLoop(uv_run_mode mode)
{
    return uv_run(env_->GetUVLoop(), mode);
}

void NapiRunner::InitProcessArgs(int argc, const char **args, int index)
{
    processArgc_ = argc;
    processArgs_ = args;
    scriptArgIndex_ = index;
}

void NapiRunner::SetNApiLogLevel(RunnerLogLevel level)
{
    switch (level)
    {
    case RunnerLogLevel::DEBUG:
        SetLogLevel(LogLevel ::Debug);
        break;
    case RunnerLogLevel::INFO:
        SetLogLevel(LogLevel ::Info);
        break;
    case RunnerLogLevel::WARNING:
        SetLogLevel(LogLevel ::Warn);
        break;
    case RunnerLogLevel::ERROR:
        SetLogLevel(LogLevel ::Error);
        break;
    case RunnerLogLevel::FATAL:
        SetLogLevel(LogLevel ::Fatal);
        break;
    default:
        DEFAULT_LOGGER(FATAL) << "Unreachable";
    }
}

panda::Local<panda::JSValueRef> NapiRunner::RequireNapi(panda::JsiRuntimeCallInfo *info)
{
    EcmaVM *vm = info->GetVM();
    panda::EscapeLocalScope scope(vm);
    NapiRunner *runner = static_cast<NapiRunner *>(info->GetData());
    Local<panda::StringRef> modulePath(info->GetCallArgRef(0));

    if (!modulePath->IsString(vm))
    {
        N_RUNNER_THROW_TYPE_ERROR(vm, "path must be string.");
        return JSValueRef::Undefined(vm);
    }

    std::string path;
    path.resize(PATH_MAX);
    modulePath->WriteUtf8(vm, path.data(), PATH_MAX, true);
    std::string realPath = utils::GetRealPath(path.c_str());
    if (realPath == "")
    {
        N_RUNNER_THROW_RANGE_ERROR(vm, (std::string("Module is not found, path: ") + path).c_str());
        return JSValueRef::Undefined(vm);
    }

    std::string errInfo = "";
    NativeModule *module =
        NativeModuleManager::GetInstance()->LoadNativeModule(realPath.c_str(), nullptr, false, errInfo);

    Local<JSValueRef> exports(JSValueRef::Undefined(vm));
    {
        auto cached = runner->loadedModules_.find(module);
        if (cached != runner->loadedModules_.end())
            return scope.Escape(cached->second.ToLocal(vm));
    }

    if (!module)
    {
        if (errInfo != "")
        {
            N_RUNNER_THROW_RANGE_ERROR(vm, errInfo.c_str());
        }
        else
        {
            N_RUNNER_THROW_RANGE_ERROR(
                vm, (std::string("Module dose not provide an entry, module path: ") + realPath).c_str());
        }

        return JSValueRef::Undefined(vm);
    }
    if (module->registerCallback)
    {
        panda::Local<panda::ObjectRef> exportObj = panda::ObjectRef::New(vm);
        napi_value result =
            module->registerCallback(reinterpret_cast<napi_env>(runner->GetEngine()), JsValueFromLocalValue(exportObj));
        exports = LocalValueFromJsValue(result);
    }
    else
    {
        return JSValueRef::Undefined(vm);
    }
    runner->loadedModules_[module] = panda::Global<panda::JSValueRef>(vm, exports);

    return scope.Escape(exports);
}

panda::Local<panda::JSValueRef> NapiRunner::PreLoad(panda::JsiRuntimeCallInfo *info)
{
    EcmaVM *vm = info->GetVM();
    panda::LocalScope scope(vm);
    NapiRunner *runner = static_cast<NapiRunner *>(info->GetData());
    ArkNativeEngine *engine = runner->GetEngine();
    Local<panda::StringRef> modulePath(info->GetCallArgRef(0));

    if (!modulePath->IsString(vm))
    {
        N_RUNNER_THROW_TYPE_ERROR(vm, "path must be string.");
        return JSValueRef::Undefined(vm);
    }

    std::string path;
    path.resize(PATH_MAX);
    modulePath->WriteUtf8(vm, path.data(), PATH_MAX, true);
    std::string realPath = utils::GetRealPath(path.c_str());
    if (realPath == "")
    {
        N_RUNNER_THROW_RANGE_ERROR(vm, (std::string("Module is not found, path: ") + path).c_str());
        return JSValueRef::Undefined(vm);
    }

    auto *work = new uv_work_t;
    work->data = new std::string(realPath);
    auto workCb = [](uv_work_t *work) {
        auto path = reinterpret_cast<std::string *>(work->data);
        std::string errInfo = "";
        if (NativeModuleManager::GetInstance()->LoadNativeModule(path->c_str(), nullptr, false, errInfo) == nullptr)
        {
            DEFAULT_LOGGER(WARNING) << "Failed to execute 'preLoad' on 'global': " << errInfo;
        };
        delete path;
    };
    if (uv_queue_work(engine->GetUVLoop(), work, workCb, [](uv_work_t *work, int status) { delete work; }) != EOK)
    {
        DEFAULT_LOGGER(WARNING) << "Failed to execute 'preLoad' on 'global':"
                                   " Failed to start async work.";
        delete reinterpret_cast<std::string *>(work->data);
        delete work;
    }
    return JSValueRef::Undefined(vm);
}

std::string GetValueStringUtf8(const EcmaVM *vm, const Local<JSValueRef> &value)
{
    if (!value->IsString(vm))
    {
        return "";
    }
    auto ref = Local<panda::StringRef>(value);
    std::string str;
    str.resize(ref->Length(vm));
    ref->WriteUtf8(vm, str.data(), str.size());
    return str;
}

// inner method, skip args check
panda::Local<panda::JSValueRef> NapiRunner::InnerExecute(panda::JsiRuntimeCallInfo *info)
{
    EcmaVM *vm = info->GetVM();
    panda::LocalScope scope(vm);

    auto path = GetValueStringUtf8(vm, info->GetCallArgRef(0));
    auto buffer = info->GetCallArgRef(1);
    Local<panda::ArrayBufferRef> bufferRef = buffer;
    auto entry = GetValueStringUtf8(vm, info->GetCallArgRef(2));
    int32_t bufSize = 0;
    auto buf = bufferRef->GetBufferAndLength(vm, &bufSize);
    auto ret = JSNApi::Execute(vm, reinterpret_cast<uint8_t *>(buf), bufSize, entry, path, false);
    return ret ? JSValueRef::True(vm) : JSValueRef::False(vm);
}
